[CDK] AppSyncにCORSレスポンスヘッダーを設定する方法を考える
こんにちは、吉川です。
早速ですが、AWS AppSyncのCORS周りが気になり、リクエストを投げてレスポンスを確認してみました。
curl -H "Origin: https://example.com" \ -H "Access-Control-Request-Method: POST" \ -X OPTIONS -v \ https://xxxxxxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
出力の一部抜粋が以下です。
< HTTP/2 200 < content-length: 0 < date: Sat, 07 May 2022 15:17:33 GMT < x-amzn-requestid: xxxxxxxxxxxxxxxxx < access-control-allow-origin: * < access-control-expose-headers: x-amzn-RequestId,x-amzn-ErrorType,x-amz-user-agent,x-amzn-ErrorMessage,Date,x-amz-schema-version < access-control-max-age: 172800 < x-cache: Miss from cloudfront < via: 1.1 xxxxxxxxxxxxxxxx.cloudfront.net (CloudFront) < x-amz-cf-pop: KIX50-P3 < x-amz-cf-id: xxxxxxxxxxxxxxxxxxxxxxxxx
このように access-control-allow-origin: *
を返しているようでした。
一応、ブラウザからリクエストする場合も確認してみます。
同じでした。
より厳しいCORS設定をしたい場合はどうすれば良いのでしょうか?今の所、その方法はAppSync内では提供されていないように見えます。
そこで、AppSyncの前段にCloudFrontを置いてCORSヘッダーを設定する方法を試してみました。
今回はmermaidで図を書いてみました。
flowchart LR client --> cloudfront subgraph aws cloudfront -- httpOrigin --> appsync -- resolver --> lambda end
Lambda関数をリゾルバとするAppSyncを用意し、その前段にCloudFrontを置くようにしていきます。この構成をCDKで実現していきます。
ただ、やや懸念の事項もあり気になる点として記載しています。また、構成がやや複雑化するトレードオフもあるため、本構成の採用を検討される場合はご留意ください。
環境
- node 16.13.0
- aws-cdk-lib 2.20.0
- @aws-cdk/aws-appsync-alpha 2.20.0-alpha.0
- constructs 10.0.115
- typescript 3.9.7
コード
フロントエンドリソースの用意
動作確認用にシンプルなフロントエンドWebアプリケーションを用意します。ページを開くとfetch APIでリクエストするだけの内容です(動きはChrome DevToolsで確認する想定)。
AppSyncをAPI認証モードで建てるので、x-api-keyヘッダと値(マネジメントコンソールで確認)を忘れずにセットします。
<!DOCTYPE html> <html> <head> <title>Example</title> </head> <body> <h1>Example</h1> </body> <script> fetch('https://xxxxxxxxxxx.cloudfront.net/graphql', { method: 'POST', headers: { 'x-api-key': 'xxxxxxxxxxxxxxxxx', 'Content-Type': 'application/graphql', }, mode: 'cors', body: JSON.stringify({ query: ` query MyQuery { user { id name } } `, }), }) </script> </html>
フロントエンドリソース分のCDKコードは以下です。
/** * Frontend */ // フロントエンド用S3バケットとCloudFrontを作成して紐付ける const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', { removalPolicy: cdk.RemovalPolicy.DESTROY, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, }) const originAccessIdentity = new cloudfront.OriginAccessIdentity( this, 'websiteOai' ) const webSiteBucketPolicyStatement = new iam.PolicyStatement({ actions: ['s3:GetObject'], effect: iam.Effect.ALLOW, principals: [ new iam.CanonicalUserPrincipal( originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId ), ], resources: [`${websiteBucket.bucketArn}/*`], }) websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement) const websiteDistribution = new cloudfront.Distribution( this, 'websiteDistribution', { defaultRootObject: 'index.html', defaultBehavior: { origin: new cloudfrontOrigins.S3Origin(websiteBucket, { originAccessIdentity, }), viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }, } ) // フロントエンドリソースもCDKでデプロイする new s3Deployment.BucketDeployment(this, 'WebsiteDeploy', { sources: [s3Deployment.Source.asset('public')], destinationBucket: websiteBucket, distribution: websiteDistribution, distributionPaths: ['/*'], })
なお、こちらの記述は次のブログをかなり参考にさせていただきました。
CloudFront DistributionのCDK Constructの新しいクラスを使って静的サイトホスティング(Amazon S3)の配信を構築してみた | DevelopersIO
レスポンスヘッダーポリシー(CORS設定)
AppSyncの前段に置くCloudFront Distributionのレスポンスヘッダーポリシーを用意します。
// レスポンスヘッダーポリシー // ここでCORS系ヘッダを追加する const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy( this, 'ResponseHeadersPolicy', { corsBehavior: { accessControlAllowCredentials: false, accessControlAllowOrigins: [`https://${websiteDistribution.domainName}`], accessControlAllowHeaders: ['*'], accessControlAllowMethods: ['POST', 'OPTIONS'], originOverride: true, }, } )
accessControlAllowOrigins: [websiteDistribution.domainName]
でフロントエンドのURLを許可するようにしています。
オリジンリクエストポリシー
CloudFrontからAppSyncにクライアントからのリクエストヘッダーを渡す必要があるためオリジンリクエストポリシーも用意します。検証の当初は未設定だったので、x-api-key
ヘッダーでAPIキーを渡しているにも関わらず以下のエラーが発生しました。
{ "errors" : [ { "errorType" : "UnauthorizedException", "message" : "You are not authorized to make this call." } ] }
これを避けるために以下のオリジンリクエストポリシーを用意しました。
// オリジンリクエストポリシー // リクエストヘッダのx-api-keyとContent-TypeをAppSyncまで渡すようにする const originRequestPolicy = new cloudfront.OriginRequestPolicy( this, 'originRequestPolicy', { headerBehavior: cloudfront.OriginRequestHeaderBehavior.allowList( 'x-api-key', 'Content-Type' ), } )
後述のコードで登場しますが、キャッシュポリシーについては cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED
と設定しています。
正直、このキャッシュポリシーとオリジンリクエストポリシーの組み合わせがベストかどうかはやや自信がないところではあります。キャッシュポリシーとオリジンリクエストポリシーについては下記のブログなども併せて参考いただければと思います。
[アップデート] Amazon CloudFront でキャッシュキーとオリジンリクエストポリシーによる管理が可能となりました | DevelopersIO
AppSync用CloudFront Distributionを作成
AppSyncの前段に置くCloudFront Distributionを作成します。
// レスポンスヘッダーポリシー // ここでCORS系ヘッダを追加する const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy( this, 'ResponseHeadersPolicy', { corsBehavior: { accessControlAllowCredentials: false, accessControlAllowOrigins: [`https://${websiteDistribution.domainName}`], accessControlAllowHeaders: ['*'], accessControlAllowMethods: ['POST'], originOverride: true, }, } )
new cloudfront.Distribution(this, 'appsyncApiDistribution', { defaultBehavior: { origin: new cloudfrontOrigins.HttpOrigin( // URLでなくDomain形式で渡さなければならないため、CFnのparseDomainName関数を使う cdk.Fn.parseDomainName(api.graphqlUrl) ), responseHeadersPolicy: { responseHeadersPolicyId: responseHeadersPolicy.responseHeadersPolicyId, }, viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, compress: false, cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, originRequestPolicy: originRequestPolicy, }, })
ややハマった点として、CloudFrontのHTTP OriginとしてAppSync APIのドメインを設定するのですが、CDKの GraphqlApi
クラスには .graphqlUrl
というURLのプロパティはあるものの、ドメインを返すプロパティやメソッドはないようです。そして、 new cloudfrontOrigins.HttpOrigin()
にはドメインを渡す必要があり、 .graphqlUrl
をそのまま渡すと次のようなエラーとなります。
Resource handler returned message: "Invalid request provided: The parameter origin name cannot contain a colon. (Service: CloudFront, Status Code: 400, Request ID: XXXXXXXXXXXX)" (RequestToken: xxxxxxxxxxxxx, HandlerErrorCode: Invalid
コロン :
が含まれてはいけないというエラーが出るので、つまり https://
の部分をカットする必要があります。ただ、CDK・CFnの仕組み上、
cloudfrontOrigins.HttpOrigin(api.graphqlUrl.replace('https://', ''))
のような方法では意図通り動作しません。ではどうするのかというと、CFnの組み込み関数である parseDomainName()
を使うと良いようでした(ちなみに、これはSlackコミュニティのcdk.devで質問して教えてもらいました)。
Lambda関数コードとGraphQLスキーマファイル
AppSyncのResolverとするLambda関数の中身は以下になります。
import { AppSyncResolverEvent } from 'aws-lambda' export const handler = async (event: AppSyncResolverEvent<{}, {}>) => { console.log(JSON.stringify({ event })) return { id: 'USER_ID', name: 'John Doe', } }
GraphQLスキーマファイルは以下です。
[gql title="schema.graphql"] type User { id: String! name: String! } type Query { user: User! } [/gql]
CDKコード全体
CDKコードの全体を以下に示します。
import { Construct } from 'constructs' import * as cdk from 'aws-cdk-lib' import * as appsync from '@aws-cdk/aws-appsync-alpha' import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs' import * as cloudfront from 'aws-cdk-lib/aws-cloudfront' import * as cloudfrontOrigins from 'aws-cdk-lib/aws-cloudfront-origins' import * as s3 from 'aws-cdk-lib/aws-s3' import * as s3Deployment from 'aws-cdk-lib/aws-s3-deployment' import * as iam from 'aws-cdk-lib/aws-iam' export class AppSyncStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props) /** * Frontend */ // フロントエンド用S3バケットとCloudFrontを作成して紐付ける const websiteBucket = new s3.Bucket(this, 'WebsiteBucket', { removalPolicy: cdk.RemovalPolicy.DESTROY, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, }) const originAccessIdentity = new cloudfront.OriginAccessIdentity( this, 'websiteOai' ) const webSiteBucketPolicyStatement = new iam.PolicyStatement({ actions: ['s3:GetObject'], effect: iam.Effect.ALLOW, principals: [ new iam.CanonicalUserPrincipal( originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId ), ], resources: [`${websiteBucket.bucketArn}/*`], }) websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement) const websiteDistribution = new cloudfront.Distribution( this, 'websiteDistribution', { defaultRootObject: 'index.html', defaultBehavior: { origin: new cloudfrontOrigins.S3Origin(websiteBucket, { originAccessIdentity, }), viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, }, } ) // フロントエンドリソースもCDKでデプロイする new s3Deployment.BucketDeployment(this, 'WebsiteDeploy', { sources: [s3Deployment.Source.asset('public')], destinationBucket: websiteBucket, distribution: websiteDistribution, distributionPaths: ['/*'], }) /** * Backend */ // AppSync API const api = new appsync.GraphqlApi(this, 'myAppsyncApi', { name: 'myAppsyncApi', schema: appsync.Schema.fromAsset('./schema.graphql'), authorizationConfig: { defaultAuthorization: { authorizationType: appsync.AuthorizationType.API_KEY, }, }, }) // レスポンスヘッダーポリシー // ここでCORS系ヘッダを追加する const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy( this, 'ResponseHeadersPolicy', { corsBehavior: { accessControlAllowCredentials: false, accessControlAllowOrigins: [websiteDistribution.domainName], accessControlAllowHeaders: ['*'], accessControlAllowMethods: ['POST'], originOverride: true, }, } ) // オリジンリクエストポリシー // リクエストヘッダのx-api-keyとContent-TypeをAppSyncまで渡すようにする const originRequestPolicy = new cloudfront.OriginRequestPolicy( this, 'originRequestPolicy', { headerBehavior: cloudfront.OriginRequestHeaderBehavior.allowList( 'x-api-key', 'Content-Type' ), } ) new cloudfront.Distribution(this, 'appsyncApiDistribution', { defaultBehavior: { origin: new cloudfrontOrigins.HttpOrigin( // URLでなくDomain形式で渡さなければならないため、CFnのparseDomainName関数を使う cdk.Fn.parseDomainName(api.graphqlUrl) ), responseHeadersPolicy: { responseHeadersPolicyId: responseHeadersPolicy.responseHeadersPolicyId, }, viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, compress: false, cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, originRequestPolicy: originRequestPolicy, }, }) // Lambda関数 const userFn = new lambdaNodejs.NodejsFunction(this, 'userFn', { entry: 'lambda-handler/find-user-handler.ts', }) // Lambda関数をDataSourceとしてAppSyncAPIと紐付ける const userDs = api.addLambdaDataSource('userDs', userFn) // schema.graphqlで定義した中のどの操作とマッピングするかを指定 userDs.createResolver({ typeName: 'Query', fieldName: 'user', }) } }
動作確認
リクエストが成功することを確認
まずはcurlコマンドで叩いてみます。
curl -H "Origin: https://{フロントエンド用CloudFrontのサブドメイン}.cloudfront.net" \ -H "Access-Control-Request-Method: POST" \ -X OPTIONS --v \ https://{AppSync用CloudFrontのサブドメイン}.cloudfront.net/graphql
出力の一部抜粋が以下です。
< HTTP/2 200 < content-length: 0 < date: Sat, 07 May 2022 15:26:38 GMT < x-amzn-requestid: xxxxxxxxxxxxxxxxxxxxxxxx < via: 1.1 xxxxxxxxxxxx.cloudfront.net (CloudFront), 1.1 xxxxxxxxxxxxxxx.cloudfront.net (CloudFront) < x-amz-cf-pop: KIX56-C2 < access-control-allow-origin: https://xxxxxxxxxxxx.cloudfront.net < access-control-allow-methods: POST < vary: Access-Control-Request-Method < vary: Origin < vary: Access-Control-Request-Headers < access-control-allow-headers: * < x-cache: Miss from cloudfront < x-amz-cf-pop: KIX50-P3 < x-amz-cf-id: xxxxxxxxxxxxx
access-control-allow-origin
と access-control-allow-methods
が意図通りに返っています。
続いて、Chromeブラウザでフロントエンド用CloudFront DistributionのURLを開き、DevToolsを確認しながら動作確認します。Networkタブを開くと以下のようにリクエストが成功していることが確認できました。
レスポンスボディも下のように意図通りの内容が返ってきていました。
{"data":{"user":{"id":"USER_ID","name":"John Doe"}}}
Access-Control-Allow-Originの値を変えてリクエストを失敗させてみる
次は、 Access-Control-Allow-Origin
を許可したいURLとは違う値にした場合に失敗することを確認します。CDKコードの accessControlAllowOrigins
の値を 'https://example.com'
に変更します。
// レスポンスヘッダーポリシー // ここでCORS系ヘッダを追加する const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy( this, 'ResponseHeadersPolicy', { corsBehavior: { accessControlAllowCredentials: false, accessControlAllowOrigins: ['https://example.com'], accessControlAllowHeaders: ['*'], accessControlAllowMethods: ['POST'], originOverride: true, }, } )
curlコマンドで叩いてみます。
curl -H "Origin: https://{フロントエンド用CloudFrontのサブドメイン}.cloudfront.net" \ -H "Access-Control-Request-Method: POST" \ -X OPTIONS --v \ https://{AppSync用CloudFrontのサブドメイン}.cloudfront.net/graphql
出力の一部抜粋が以下です。
< HTTP/2 200 < content-length: 0 < date: Sat, 07 May 2022 15:32:51 GMT < x-amzn-requestid: xxxxxxxxxxxxxx < via: 1.1 xxxxxxxxxxxxxx.cloudfront.net (CloudFront), 1.1 xxxxxxxxxxxxxxxx.cloudfront.net (CloudFront) < x-amz-cf-pop: KIX56-C2 < access-control-allow-origin: https://example.com < access-control-allow-methods: POST < vary: Access-Control-Request-Method < vary: Origin < vary: Access-Control-Request-Headers < access-control-allow-headers: * < x-cache: Miss from cloudfront < x-amz-cf-pop: KIX50-P3 < x-amz-cf-id: xxxxxxxxxxxxxxxxxxxxxxxxxxx
access-control-allow-origin
が https://example.com
になっています。
Chromeブラウザで確認するとエラーになることがわかります。
また、DevToolsのConsoleタブにもエラーが出力されます。
Access to fetch at 'https://xxxxxxxxxx.cloudfront.net/graphql' from origin 'https://xxxxxxxxxxx.cloudfront.net' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
これでリクエスト元ページのドメインが違う場合は弾かれることを確認できました。
気になる点
オリジンのAppSync APIのURLが知られていれば直接叩けてしまう?
気になる点のひとつに、上記を実施してもオリジンのAppSync APIのURL https://xxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
を知っている人は直接叩くことができてしまう問題があります。
これに対してはまず、AppSync APIのURLは推測困難と思われることから、割り切って更なる対処はしないという選択肢が考えられます。
「もしURLを知られたとしてもブロックしたい」という場合は、実際に試してはいないのですが、AppSyncとAWS WAFを紐付けて、下記のCloudFrontのIPレンジをIP制限のホワイトリストに設定するという方法が採れるかもしれないと思っています。
CloudFront エッジサーバーの場所と IP アドレス範囲 - Amazon CloudFront
AWS WAFのIP制限については下記ブログなどが参考になりそうです。
AWS WAFV2でIPアドレス制限してみた | DevelopersIO
Subscriptionには影響はない?
CloudFrontを前段に置くことでAppSyncのSubscriptionに影響が発生するかどうかについては、申し訳ないのですがまだ手元で試せていないため何とも断言が難しいです。後日検証ブログを投稿したいと考えています。
本記事の構成を検討しているが、要件的にSubscriptionが必要そう……という場合はこの点しっかり事前リサーチをした方が良さそうです。
その他補足
AppSyncのカスタムレスポンスヘッダーサポートは使えない?
AWS AppSync がカスタムレスポンスヘッダーのサポートを追加
最近、上のようなアップデートがあり、 $util.http.addResponseHeader()
が使えるようになりました。ただ、以下の制約があるようです。
$util.http の HTTP ヘルパー - AWS AppSync
以下の制限が適用されます。 ヘッダー名は、既存のヘッダー名または制限付きのいずれとも一致できませんAWSまたはAWS AppSync ヘッダー。 ヘッダー名を制限付きプレフィックスで始めることはできません。x-amzn-またはx-amz-。 カスタムレスポンスヘッダーのサイズは 4 KB を超えることはできません。これには、ヘッダーの名前と値が含まれます。 各レスポンスヘッダーは、GraphQL オペレーションごとに 1 回定義する必要があります。ただし、同じ名前のカスタムヘッダーを複数回定義すると、最新の定義が応答に表示されます。すべてのヘッダーは、名前に関係なく、ヘッダーサイズの制限にカウントされます。
上記より、既存のヘッダー(つまり Access-Control-Allow-Origin
)を上書きすることはできないように見えます。実際に試してみました。
const apiDistribution = new cloudfront.Distribution( this, 'appsyncApiDistribution', { defaultBehavior: { origin: new cloudfrontOrigins.HttpOrigin( // URLでなくDomain形式で渡さなければならないため、CFnのparseDomainName関数を使う cdk.Fn.parseDomainName(api.graphqlUrl) ), responseHeadersPolicy: { responseHeadersPolicyId: responseHeadersPolicy.responseHeadersPolicyId, }, viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, compress: false, cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, originRequestPolicy: originRequestPolicy, }, } ) // Lambda関数 const userFn = new lambdaNodejs.NodejsFunction(this, 'userFn', { entry: 'lambda-handler/find-user-handler.ts', }) // Lambda関数をDataSourceとしてAppSyncAPIと紐付ける const userDs = api.addLambdaDataSource('userDs', userFn) // schema.graphqlで定義した中のどの操作とマッピングするかを指定 userDs.createResolver({ typeName: 'Query', fieldName: 'user', responseMappingTemplate: appsync.MappingTemplate.fromString(` $util.http.addResponseHeader("x-example-header", "example-value") $util.http.addResponseHeader("Access-Control-Allow-Origin", "https://${apiDistribution.domainName}") $util.http.addResponseHeader("Access-Control-Allow-Origin-2", "https://${apiDistribution.domainName}") $util.toJson($context.result) `), })
- x-example-header
- Access-Control-Allow-Origin
- Access-Control-Allow-Origin-2
の3つのヘッダーを加えてみました。そして確認した結果が以下です。
OPTIONSリクエスト:
POSTリクエスト:
POSTリクエストにおいてx-example-headerとAccess-Control-Allow-Origin-2は追加できました。しかし、Access-Control-Allow-Originの値は *
のままでありやはり上書きできないように見えます。
また、OPTIONSつまりPreflightリクエストでは追加ヘッダーが含まれない結果となりました。ResponseMappingTemplateはResolverごとに設定するので、OPTIONSリクエストはどのResolverにも該当しないためと思われます。この点はどのようなリクエストに対しても設定できるような項目があればクリアできる可能性がありますが、自分が確認した限りでは見つけられませんでした(もしできるという情報があれば提供頂ければ嬉しいです)。
以上の2点から、現時点では $util.http.addResponseHeader()
でCORSを設定することは難しいと考えています。
まとめ
ここまでAppSyncの前段にCloudFrontを置いてCORSレスポンスヘッダーを設定する方法を紹介しましたが、この構成が常に推奨かというと否であり、ケースバイケースの意思決定が必要と思います。
構成がやや複雑になるという点でトレードオフは発生するので、要件を鑑みつつ総合的に判断していきましょう。
以上、少しでも参考になれば幸いです。
参考
- ネルさんはTwitterを使っています: 「なるほどAppSync確かにこりゃ楽だ。GraphQL覚えるのがめんどいのと、API Gatewayと比べてどれだけ柔軟性があるかってところだとは思うけど。追々調査してみよう。」 / Twitter
- AWS AppSync+TerraformでサーバレスなWebアプリケーションを自動作成する - Qiita
- CloudFront DistributionのCDK Constructの新しいクラスを使って静的サイトホスティング(Amazon S3)の配信を構築してみた | DevelopersIO
- class Fn · AWS CDK
- L@EやCF2を使わずにレスポンスヘッダーを設定するCloudFront DistributionをAWS CDKで構築してみた | DevelopersIO
- GitHubでmermaid記法が使えるようになったのでアーキテクチャーの図を書いてみた | DevelopersIO
- GraphQL APIをfetchメソッドで叩く方法
- AppSync & GraphQL 入門 - Qiita
- [アップデート] Amazon CloudFront でキャッシュキーとオリジンリクエストポリシーによる管理が可能となりました | DevelopersIO
- CloudFront エッジサーバーの場所と IP アドレス範囲 - Amazon CloudFront
- CORSリクエストでクレデンシャル(≒クッキー)を必要とする場合の注意点 - Qiita
- なんとなく CORS がわかる...はもう終わりにする。 - Qiita
- 認証と認可 - AWS AppSync
- CORS許可されているかをcURLで確認する方法 - Qiita
- WebSocket API Gateway の前にCloudFrontを置く | DevelopersIO